package org.hepeng.workx.extension;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.EnumerationUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.hepeng.workx.exception.ApplicationRuntimeException;
import org.hepeng.workx.exception.NoSuchConstructorException;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.DefaultResourceLoader;

import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author he peng
 */

public final class XLoader<T> {

    private static final Map<Class<?> , XLoader<?>> X_LOADER_MAP = new ConcurrentHashMap<>();
    private final Map<String, Class<?>> X_CLASS_MAP = new ConcurrentHashMap<>();
    private final Map<String , T> defaultConstructorInstanceCacheMap = new ConcurrentHashMap<>();

    private final String WORKX_DIRECTORY = "META-INF/workx/";
    private final String xPointDirectory;
    private final String wholeXPointDirectory;
    private Class<?> xPointClass;
    private String defaultXName;
    private Class<?> defaultXClass;
    private T defaultXNoArgConstructorInstance;

    public XLoader(Class<?> xPointClass) {
        this(xPointClass , "");
    }

    public XLoader(Class<?> xPointClass , String xPointDirectory) {
        this.xPointClass = xPointClass;
        if (StringUtils.isNotBlank(StringUtils.trim(xPointDirectory))) {
            this.xPointDirectory = StringUtils.endsWith(xPointDirectory, "/") ? xPointDirectory : xPointDirectory + "/";
        } else {
            this.xPointDirectory = "";
        }
        this.wholeXPointDirectory = WORKX_DIRECTORY + this.xPointDirectory;

        XPoint xPoint = AnnotationUtils.findAnnotation(xPointClass, XPoint.class);
        if (Objects.nonNull(xPoint)) {
            this.defaultXName = StringUtils.lowerCase(xPoint.value());
        }
        loadXClass();
    }

    public T getX() {
        if (Objects.isNull(this.defaultXNoArgConstructorInstance)) {
            this.defaultXNoArgConstructorInstance = getX(null , null);
        }
        return this.defaultXNoArgConstructorInstance;
    }

    public T getX(String name) {
        name = StringUtils.lowerCase(name);
        T instance = defaultConstructorInstanceCacheMap.get(name);
        if (Objects.isNull(instance)) {
            instance = getX(name, null, null);
            defaultConstructorInstanceCacheMap.put(name , instance);
        }
        return instance;
    }

    public T getX(String name , List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        name = StringUtils.lowerCase(name);
        Class<?> xClass = X_CLASS_MAP.get(name);
        if (Objects.nonNull(xClass)) {
            Class<?>[] argTypes = org.hepeng.workx.util.ConstructorUtils.argTypeToArray(constructorArgTypes);
            Constructor<?> constructor = ConstructorUtils.getAccessibleConstructor(xClass, argTypes);
            if (Objects.nonNull(constructor)) {
                Object[] args = org.hepeng.workx.util.ConstructorUtils.argToArray(constructorArgs);
                try {
                    return (T) ConstructorUtils.invokeConstructor(xClass, args);
                } catch (Throwable t) {
                    throw new ApplicationRuntimeException(t);
                }
            }
        }

        return null;
    }

    public T getX(List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        T xImpl;
        try {
            if (Objects.nonNull(this.defaultXClass)) {
                xImpl = getDefaultX(constructorArgTypes, constructorArgs);
            } else {
                xImpl = getXConstructorIfAvailable(constructorArgTypes , constructorArgs);
            }
        } catch (Throwable t) {
            throw new ApplicationRuntimeException(t);
        }

        if (Objects.isNull(xImpl)) {
            System.err.println("defaultXName ==> " + this.defaultXName + " , defaultXClass ==> " + defaultXClass);
            throw new NoSuchConstructorException(
                    "No extension ( " + this.xPointClass + " ) class found satisfying the specified constructor argument (" + constructorArgTypes + ")");
        }

        return xImpl;
    }

    private T getXConstructorIfAvailable(List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws Exception {
        if (MapUtils.isEmpty(X_CLASS_MAP)) {
            return null;
        }

        for (Map.Entry<String, Class<?>> entry : X_CLASS_MAP.entrySet()) {
            Class<?> xClass = entry.getValue();
            Class<?>[] argTypes = org.hepeng.workx.util.ConstructorUtils.argTypeToArray(constructorArgTypes);
            Constructor<?> constructor = ConstructorUtils.getAccessibleConstructor(xClass, argTypes);
            if (Objects.nonNull(constructor)) {
                Object[] args = org.hepeng.workx.util.ConstructorUtils.argToArray(constructorArgs);
                return (T) ConstructorUtils.invokeConstructor(xClass, args);
            }
        }
        return null;
    }

    private T getDefaultX(List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws Exception {
        if (CollectionUtils.isEmpty(constructorArgTypes) || CollectionUtils.isEmpty(constructorArgs)) {
            return (T) this.defaultXClass.newInstance();
        }

        Class<?>[] argTypes = org.hepeng.workx.util.ConstructorUtils.argTypeToArray(constructorArgTypes);
        Constructor<?> constructor = org.springframework.util.ClassUtils.getConstructorIfAvailable(this.defaultXClass, argTypes);

        if (Objects.nonNull(constructor)) {
            Object[] args = org.hepeng.workx.util.ConstructorUtils.argToArray(constructorArgs);
            return (T) ConstructorUtils.invokeConstructor(this.defaultXClass , args);
        }

        return null;
    }

    public static <T> XLoader<T> getXLoader(Class<T> xPointClass) {
        return getXLoader(xPointClass , "");
    }

    public static <T> XLoader<T> getXLoader(Class<T> xPointClass , String xPointDirectory) {
        XLoader<?> xLoader = X_LOADER_MAP.get(xPointClass);
        if (Objects.isNull(xLoader)) {
            X_LOADER_MAP.put(xPointClass , new XLoader<>(xPointClass , xPointDirectory));
        }
        return (XLoader<T>) X_LOADER_MAP.get(xPointClass);
    }

    private void loadXClass() {
        String xPointFileName = this.wholeXPointDirectory + this.xPointClass.getName();
        ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
        List<URL> urlList = new ArrayList<>();
        try {
            Enumeration<URL> urls;
            if (Objects.nonNull(classLoader)) {
                urls = classLoader.getResources(xPointFileName);
            } else {
                urls = ClassLoader.getSystemResources(xPointFileName);
            }

            if (Objects.isNull(urls) || ! urls.hasMoreElements()) {
                DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
                URL url = defaultResourceLoader.getResource(xPointFileName).getURL();
                urlList.add(url);
            } else {
                urlList.addAll(EnumerationUtils.toList(urls));
            }
        } catch (Throwable t) {
            throw new ApplicationRuntimeException("Exception when load extension file( " + xPointFileName + ")." , t);
        }

        if (CollectionUtils.isEmpty(urlList)) {
            return;
        }

        try {
            for (URL url : urlList) {
                X_CLASS_MAP.putAll(readXPointFile(url));
            }
        } catch (Throwable t) {
            throw new ApplicationRuntimeException("Exception when load extension class( " + this.xPointClass + ")." , t);
        }
    }

    private Map<String , Class<?>> readXPointFile(URL url) throws Exception {
        List<String> lines = IOUtils.readLines(new InputStreamReader(url.openStream(), Charset.forName("utf-8")));
        if (CollectionUtils.isEmpty(lines)) {
            return null;
        }

        Map<String , Class<?>> xClassMap = new HashMap<>();
        for (String line : lines) {
            final int ci = line.indexOf('#');
            if (ci >= 0) {
                line = line.substring(0, ci);
            }
            line = line.trim();

            if (StringUtils.isBlank(line)) {
                continue;
            }

            int equalSymbolIndex = StringUtils.indexOf(line, "=");
            if (equalSymbolIndex < 0) {
                continue;
            }

            String xName = StringUtils.lowerCase(StringUtils.substring(line, 0, equalSymbolIndex).trim());
            String xClassName = StringUtils.substring(line , equalSymbolIndex + 1).trim();
            ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
            classLoader = Objects.nonNull(classLoader) ? classLoader : Thread.currentThread().getContextClassLoader();
            Class<?> xClass = ClassUtils.getClass(classLoader, xClassName, true);
            if (! this.xPointClass.isAssignableFrom(xClass)) {
                throw new IllegalStateException("Error when load extension class( " +
                        this.xPointClass + ", class line: " + xClass.getName() + "), class "
                        + xClass.getName() + "is not subtype of " + this.xPointClass);
            }
            xClassMap.put(xName , xClass);

            if (StringUtils.equals(this.defaultXName , xName)) {
                this.defaultXClass = xClass;
            }

            DefaultXImpl defaultX = AnnotationUtils.findAnnotation(xClass, DefaultXImpl.class);
            if (Objects.nonNull(defaultX)) {
                this.defaultXClass = xClass;
            }
        }

        return xClassMap;
    }
}
